package cn.funnymap.lgis.tools.vector.parser;

import cn.funnymap.lgis.tools.crs.CRSUtil;
import cn.funnymap.lgis.tools.exception.ShpParserRuntimeException;
import cn.funnymap.lgis.tools.exception.ShpParserValidationException;
import cn.funnymap.lgis.tools.file.FileType;
import cn.funnymap.lgis.tools.file.FileUtil;
import org.apache.commons.lang3.StringUtils;
import org.geotools.data.FeatureSource;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.feature.DefaultFeatureCollection;
import org.geotools.referencing.CRS;
import org.locationtech.jts.geom.Geometry;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Shapefile解析器抽象类
 *
 * @author jiaoxn
 */
public abstract class ShpParser<T> {
    // 读取Shapefile文件时其他所需文件的类型：DBF、SHX、PRJ
    private static final List<FileType> OTHER_REQUIRED_FILE_TYPES =
            Arrays.asList(FileType.DBF, FileType.SHX, FileType.PRJ);
    private final boolean deleteFileWhenDone;

    final ShpProcessorParameter processorParameter;

    protected ShpParser(ShpParseParameter parseParameter) {
        processorParameter = ShpProcessorParameter.build(parseParameter);

        this.deleteFileWhenDone = parseParameter.getDeleteFileWhenDone();
    }

    public T execute() {
        try {
            this.dataPreprocessing();
            return this.parseShapefile();
        } catch (IOException e) {
            throw new ShpParserRuntimeException("Shapefile文件解析失败，" + e.getCause());
        } finally {
            this.deleteRelatedFile();
        }
    }
    abstract T parseShapefile();

    /*--------------------------------------------------数据预处理-开始------------------------------------------------*/
    private void dataPreprocessing() throws IOException {
        Preprocessor<IOException> decompressPreprocessor = this::buildDecompressPreprocessor;
        Preprocessor<IOException> integrityCheckPreprocessor = this::checkShpIntegrity;
        decompressPreprocessor.andThen(integrityCheckPreprocessor).run();
    }
    private void buildDecompressPreprocessor() throws IOException {
        Path inputFilePath = processorParameter.getInputFilePath();

        // 如果输入文件是ZIP文件，则先解压
        if (FileUtil.validateFileType(inputFilePath, FileType.ZIP)) {
            Path decompressionPath = FileUtil.extractZipFileToTemp(inputFilePath);
            List<Path> shpFileList = FileUtil.getFileByFileExtension(decompressionPath, FileType.SHP.getValue());
            processorParameter.setDecompressionDirPath(decompressionPath);
            processorParameter.setShpFilePathList(shpFileList);
        } else {
            processorParameter.setShpFilePathList(Collections.singletonList(inputFilePath));
        }
    }
    private void checkShpIntegrity() throws IOException {
        for (Path path : processorParameter.getShpFilePathList()) {
            String shpFilename = FileUtil.getFileNameWithoutExtension(path);
            Path parentDir = path.getParent();

            List<String> otherRequiredFilenameList = combineOtherRequiredFilenameList(shpFilename);
            List<Path> otherRequiredFilePathList =
                    FileUtil.getFileByFileName(parentDir, otherRequiredFilenameList, false);
            if (otherRequiredFilePathList.isEmpty()) {
                // 删除相关的文件或者文件夹
                deleteRelatedFile();

                String exceptionMessage =
                        String.format("Shapefile文件完整性校验失败，请检查%s等文件是否存在",
                                StringUtils.join(otherRequiredFilenameList, "、"));
                throw new ShpParserValidationException(exceptionMessage);
            }
        }
    }
    private List<String> combineOtherRequiredFilenameList(String filename) {
        return OTHER_REQUIRED_FILE_TYPES
                .stream()
                .map(item -> String.format("%s%s", filename, item.getValue()))
                .collect(Collectors.toList());
    }
    /*--------------------------------------------------数据预处理-结束------------------------------------------------*/

    SimpleFeatureCollection readAndTransform(Path shpFilePath) throws IOException {
        ShapefileDataStore shapefileDataStore = this.createDataStore(shpFilePath);
        return readAndTransformSimpleFeature(shapefileDataStore, processorParameter.getTargetCrs());
    }
    private SimpleFeatureCollection readAndTransformSimpleFeature(
            ShapefileDataStore shapefileDataStore, CoordinateReferenceSystem targetCrs) throws IOException {
        boolean isSameCrs = CRSUtil.equals(shapefileDataStore.getFeatureSource().getInfo().getCRS(), targetCrs);

        DefaultFeatureCollection featureCollection = new DefaultFeatureCollection();
        try (SimpleFeatureIterator iterator = shapefileDataStore.getFeatureSource().getFeatures().features()) {
            while (iterator.hasNext()) {
                SimpleFeature simpleFeature = iterator.next();

                // 如果源数据的空间参考和目标空间参考不一致，则需要进行空间变换
                if (!isSameCrs) {
                    Geometry geometry = this.transform(simpleFeature, targetCrs);
                    simpleFeature.setDefaultGeometry(geometry);
                }

                featureCollection.add(simpleFeature);
            }
        }

        return featureCollection;
    }

    List<ShpAttributeField> readAttributeField(Path shpFilePath) throws IOException {
        ShapefileDataStore shapefileDataStore = new ShapefileDataStore(shpFilePath.toUri().toURL());
        FeatureSource<SimpleFeatureType, SimpleFeature> featureSource = shapefileDataStore.getFeatureSource();
        SimpleFeatureType simpleFeatureType = featureSource.getSchema();
        List<AttributeDescriptor> attributeDescriptorList = simpleFeatureType.getAttributeDescriptors();
        return attributeDescriptorList.stream()
                .filter(item -> !"the_geom".equalsIgnoreCase(item.getName().toString()))
                .map(this::attributeDescriptor2Def)
                .collect(Collectors.toList());
    }
    private ShpAttributeField attributeDescriptor2Def(AttributeDescriptor attributeDescriptor) {
        String fieldName = attributeDescriptor.getName().toString();
        // 读取字段类型后，转为纯小写
        String fieldType = attributeDescriptor.getType().getName().toString().toLowerCase();
        return new ShpAttributeField(fieldName, fieldName, fieldType, null);
    }

    private ShapefileDataStore createDataStore(Path shpPath) throws IOException {
        ShapefileDataStore shapefileDataStore = new ShapefileDataStore(shpPath.toUri().toURL());

        // 通过.dbf文件读取文件编码，并设置编码【防止中文乱码】
        String dbfFilePath = shpPath.toAbsolutePath().toString().replace(".shp", ".dbf");
        Path dbfPath = Paths.get(dbfFilePath);
        String encoding = FileUtil.detectEncoding(dbfPath);
        shapefileDataStore.setCharset(Charset.forName(encoding));

        return shapefileDataStore;
    }
    private Geometry transform(SimpleFeature simpleFeature, CoordinateReferenceSystem targetCrs) {
        return CRSUtil.transform(
                (Geometry) simpleFeature.getDefaultGeometry(),
                simpleFeature.getType().getCoordinateReferenceSystem(),
                targetCrs);
    }
    void deleteRelatedFile() {
        if (this.deleteFileWhenDone) {
            if (processorParameter.getInputFilePath().isAbsolute()) {
                FileUtil.delete(processorParameter.getInputFilePath());
            }

            if (processorParameter.isZip()) {
                FileUtil.delete(processorParameter.getDecompressionDirPath());
            }
        }
    }
}

@FunctionalInterface
interface Preprocessor<E extends Exception> {
    void run() throws E;

    default Preprocessor<E> andThen(Preprocessor<E> after) {
        Objects.requireNonNull(after);
        return () -> {
            run();
            after.run(); };
    }
}
